昨天,我們使用 server.getPrimaryServices()
這張「全景地圖」,我們成功地找到了島上所有隱藏的洞穴(服務),並在我們的 UI 上為它們一一建立了標記。我們的網頁不再是空白一片,而是動態地展示了裝置的宏觀結構。
但是,洞穴的入口並不是我們的終點。我們真正的目標,是洞穴深處閃閃發光的寶藏——那些真正承載著數據、等待我們讀取和寫入的「特徵 (Characteristics)」。心率數值、電池電量、溫度讀數... 所有這些有價值的資訊,都存放在特徵之中。
今天,我們的任務就是深入每一個已發現的服務洞穴,點亮火把,照亮其中的每一條通道,找到所有隱藏的寶箱。我們將使用的關鍵指令是 service.getCharacteristics()
。這一步,是我們從「宏觀探索」轉向「微觀尋寶」的決定性一步,也是我們能與裝置進行實質性數據交換前的最後一道關卡。
service.getCharacteristics()
這是我們深入服務內部,尋找寶藏的關鍵工具。
隸屬:BluetoothGATTService
物件的方法。也就是我們昨天在 for...of
迴圈中得到的那個 service
物件。
非同步:它同樣是一個非同步操作,會回傳一個 Promise
。
回傳值:當 Promise 成功解析時,它會回傳一個陣列。這個陣列中,包含了該服務下所有特徵的 BluetoothGATTCharacteristic
物件。
characteristic.properties
API 回傳的每一個 characteristic
物件,都附帶一個名為 properties
的屬性。這個屬性本身也是一個物件,它像一本「特徵能力說明書」,用布林值(true
/false
)告訴我們這個特徵到底能做什麼。
// 一個 characteristic.properties 物件的範例
{
read: true, // 這個特徵的值可以被讀取
write: false, // 不可寫入
notify: true, // 可以被訂閱 (當值改變時會主動通知我們)
indicate: false, // 另一種通知形式
writeWithoutResponse: false // 無需回應的寫入
// ... 等等
}
這個 properties
物件,正是我們 Day 13/14 打造的 renderCharacteristic
函式所需要的最關鍵的「原料」!
我們的策略是,在昨天遍歷「服務」的 for...of
迴圈內部,再嵌套一個新的迴圈來遍歷「特徵」。
打開 app.js
,找到昨天的 for (const service of services)
迴圈,並在其中加入探索特徵的邏輯:
// app.js -> scanButton.onclick -> for...of 迴圈內部
for (const service of services) {
log(`>> 正在處理服務: ${service.uuid}`);
// ... (昨天儲存 service 和創建 serviceCard 的程式碼保持不變) ...
const serviceCard = gattProfile.services[service.uuid].uiCard;
// ---- ↓↓↓ 今天新增的核心程式碼 ↓↓↓ ----
try {
log(`---> 正在探索特徵...`);
const characteristics = await service.getCharacteristics();
log(`---> 發現 ${characteristics.length} 個特徵!`);
// 再次使用 for...of 迴圈,遍歷這個服務下的所有特徵
for (const characteristic of characteristics) {
log(`-----> 特徵 UUID: ${characteristic.uuid}`);
// 步驟 1: 將特徵資訊完整地存入我們的資料模型
gattProfile.services[service.uuid].characteristics[characteristic.uuid] = {
uuid: characteristic.uuid,
properties: characteristic.properties, // 儲存最關鍵的屬性物件
instance: characteristic, // 保存原始實例
value: null // 準備好存放數值的空間
};
// 步驟 2: 將真實的特徵資訊,餵給我們最強大的 UI 工廠函式!
renderCharacteristic(
{ // 傳入我們需要的資訊
uuid: characteristic.uuid,
properties: characteristic.properties
},
serviceCard // 告訴函式要把 UI 渲染到哪張服務卡片上
);
}
} catch(error) {
log(`-----> 探索特徵失敗: ${error.message}`);
}
// ---- ↑↑↑ 今天新增的核心程式碼 ↑↑↑ ----
}
嵌套迴圈:我們在處理單一 service
的迴圈內部,呼叫了 service.getCharacteristics()
,然後再用一個新的 for...of
迴圈來處理返回的 characteristics
陣列。這形成了一個完美的「服務 -> 特徵」的探索結構。
完整的資料模型:我們現在將 characteristic.uuid
、characteristic.properties
和 characteristic
的原始實例,全部存入了 gattProfile
中對應的服務底下。至此,我們的 gattProfile
物件已經完整地映射了真實裝置的 GATT 結構!
renderCharacteristic
的威力:還記得我們在 Day 14 精心打造的那個函式嗎?現在是它大顯神威的時候了!我們將從真實裝置獲取到的 uuid
和 properties
傳遞給它。它會自動判斷:如果 properties.read
為 true
,就生成一個「Read」按鈕;如果 properties.write
為 true
,就生成一個輸入框和「Write」按鈕...
現在,重新整理頁面,連接到你的藍牙裝置。你會看到,網頁上不僅動態生成了服務卡片,每個服務卡片下面,還根據該服務真實擁有的特徵,動態地生成了對應的、功能完備的互動面板!
今天,我們完成了尋寶之旅中最關鍵、也是最後的探索步驟!
至此,我們應用程式的「探索」階段已完美收官。我們能根據根據目標裝置的特性,自動準備就緒。
我們已經找到了所有的寶箱,並且知道了每個寶箱上面是掛著「讀取鎖」還是「寫入鎖」。從明天開始,我們將學習如何打開這些鎖。我們將從最直接的操作——讀取 (Read)——開始,學習 characteristic.readValue()
,並第一次從裝置中,將真實的數據讀取到我們的網頁上!
那麼今天的內容就到這邊,感謝你能看到這裡,在這邊祝你早安、午安、晚安,我們明天見。